package ca.scotthyndman.as2haxe.dom.binding;

import java.util.HashMap;
import java.util.Map;

import org.apache.log4j.Logger;

import ca.scotthyndman.as2haxe.dom.ASExpression;
import ca.scotthyndman.as2haxe.dom.ASFieldAccess;
import ca.scotthyndman.as2haxe.dom.ASFunction;
import ca.scotthyndman.as2haxe.dom.ASImportDeclaration;
import ca.scotthyndman.as2haxe.dom.ASMethodInvocation;
import ca.scotthyndman.as2haxe.dom.ASName;
import ca.scotthyndman.as2haxe.dom.ASPackage;
import ca.scotthyndman.as2haxe.dom.AST;
import ca.scotthyndman.as2haxe.dom.ASTNode;
import ca.scotthyndman.as2haxe.dom.ASType;
import ca.scotthyndman.as2haxe.dom.ASTypeDeclaration;
import ca.scotthyndman.as2haxe.dom.ASVariableDeclaration;
import ca.scotthyndman.as2haxe.dom.IScopeElement;
import ca.scotthyndman.as2haxe.dom.binding.impl.MethodBinding;
import ca.scotthyndman.as2haxe.dom.binding.impl.PackageBinding;
import ca.scotthyndman.as2haxe.dom.binding.impl.TypeBinding;
import ca.scotthyndman.as2haxe.dom.binding.impl.VariableBinding;
import ca.scotthyndman.as2haxe.dom.visitor.ASDomVisitorBase;

/**
 * Is responsible for handling an AST's bindings.
 */
public class BindingManager {

	/** Logger for this class */
	private static final Logger log = Logger.getLogger(BindingManager.class);

	/** The AST */
	private AST ast;

	/** Visitor responsible for exposing bindings */
	private BindingExposer exposer;

	/** Visitor responsible for linking bindings with references */
	private BindingLinker linker;

	/** Maps binding keys to their bindings */
	private Map<String, IBinding> keyToBinding = new HashMap<String, IBinding>();

	/** Maps referencing nodes to their bindings */
	private Map<ASTNode, IBinding> refToBinding = new HashMap<ASTNode, IBinding>();

	/** Maps AST source nodes to their bindings */
	private Map<ASTNode, IBinding> sourceToBinding = new HashMap<ASTNode, IBinding>();

	/**
	 * Constructs a new binding manager.
	 */
	public BindingManager(AST ast) {
		this.ast = ast;
		this.exposer = new BindingExposer(this);
		this.linker = new BindingLinker(this);
	}

	/**
	 * Returns this manager's AST.
	 * 
	 * @return the AST
	 */
	public AST getAST() {
		return ast;
	}

	/**
	 * Resolves a type binding.
	 * 
	 * @param typeRef
	 *            reference to the type
	 * @return the binding
	 */
	public ITypeBinding resolveTypeBinding(ASType typeRef) {
		if (refToBinding.containsKey(typeRef)) {
			return (ITypeBinding) refToBinding.get(typeRef);
		}

		ITypeBinding binding = null;
		try {
			binding = (ITypeBinding) refToBinding.get(typeRef);
		} catch (RuntimeException e) {
		}

		if (binding == null) {
			log.warn("No binding found for " + typeRef.toString());
			return null;
		}

		return binding;
	}

	/**
	 * Resolves a variable binding.
	 * 
	 * @param fieldRef
	 *            reference to the field
	 * @return the binding
	 */
	public IBinding resolveFieldAccessBinding(ASFieldAccess fieldRef) {
		if (refToBinding.containsKey(fieldRef)) {
			return refToBinding.get(fieldRef);
		}

		IBinding binding = null;
		try {
			binding = (IBinding) refToBinding.get(fieldRef);
		} catch (RuntimeException e) {
		}

		if (binding == null) {
			log.warn("No binding found for " + fieldRef.toString());
			return null;
		}

		return binding;
	}

	/**
	 * Resolves an import to the corresponding package or type binding.
	 * 
	 * @param importDecl
	 *            The import declaration to resolve.
	 * @return the binding
	 */
	public IBinding resolveImportBinding(ASImportDeclaration importDecl) {
		//
		// This is an import binding
		//
		if (importDecl.isOnDemand()) {
			ASName packageName = importDecl.getName().getQualifier();
			return getBinding(packageName.toString());
		} 
		
		//
		// This is a type binding
		//
		return getBinding(importDecl.getName().toString());
	}

	/**
	 * Resolves a method binding.
	 * 
	 * @param methRef
	 *            reference to the field
	 * @return the binding
	 */
	public IMethodBinding resolveMethodBinding(ASMethodInvocation methRef) {
		if (refToBinding.containsKey(methRef)) {
			return (IMethodBinding) refToBinding.get(methRef);
		}

		IMethodBinding binding = null;
		try {
			binding = (IMethodBinding) refToBinding.get(methRef);
		} catch (RuntimeException e) {
		}

		if (binding == null) {
			log.warn("No binding found for " + methRef.toString());
			return null;
		}

		return binding;
	}

	/**
	 * Creates a link between a binding and a node.
	 * 
	 * @param ref
	 * @param binding
	 */
	protected void createBindingLink(ASTNode ref, IBinding binding) {
		if (log.isInfoEnabled()) {
			log.info("creating link between " + ref.toString() + " and "
					+ binding.toString());
		}

		binding.getReferringNodes().add(ref);
		refToBinding.put(ref, binding);
	}

	/**
	 * Exposes the AST's bindings.
	 */
	public void exposeBindings() {

		ASPackage.getRootPackage().accept(exposer, true);
	}

	/**
	 * Exposes a single node if possible.
	 * 
	 * @param pkg
	 */
	public void expose(ASTNode pkg) {
		pkg.accept(exposer, false);
	}

	/**
	 * Exposes a binding to the manager.
	 * 
	 * @param binding
	 *            the binding to expose
	 */
	protected void exposeBinding(IBinding binding) {
		log.info("Exposing binding: " + binding.toString());
		keyToBinding.put(binding.getKey(), binding);
		sourceToBinding.put(binding.getASTNode(), binding);
	}

	/**
	 * Changes the name of a binding.
	 * 
	 * @param oldName
	 *            the old name
	 * @param newName
	 *            the new name
	 */
	public void renameBinding(String oldName, String newName) {
		keyToBinding.put(newName, keyToBinding.remove(oldName));

	}

	/**
	 * Gets a binding with a specific key.
	 * 
	 * @param key
	 *            the binding's key
	 * @return the binding, or <code>null</code> if not found
	 */
	public IBinding getBinding(String key) {
		return keyToBinding.get(key);
	}

	/**
	 * Returns the bindings associated with a given declaration
	 * 
	 * @param src
	 *            the declration
	 * @return the binding, or <code>null</code> if not found
	 */
	public IBinding getBindingBySource(ASTNode src) {
		return sourceToBinding.get(src);
	}

	/**
	 * Links bindings with their references.
	 */
	public void linkBindings() {
		ASPackage.getRootPackage().accept(linker, true);
	}

	/**
	 * Re-links a node subtree.
	 * 
	 * @param node
	 */
	public void link(ASTNode node) {
		node.accept(linker, true);
	}

	/**
	 * Exposes bindings in a source tree.
	 */
	protected class BindingExposer extends ASDomVisitorBase {

		/** The manager */
		private BindingManager manager;

		/** The current type */
		private ASTypeDeclaration currentType;

		/**
		 * Constructs a new binding exposer
		 * 
		 * @param mgr
		 *            The binding manager
		 */
		public BindingExposer(BindingManager mgr) {
			this.manager = mgr;
		}

		/**
		 * Exposes a type
		 */
		public boolean visit(ASTypeDeclaration type) {
			TypeBinding binding = new TypeBinding(type, type
					.getName().toString());
			binding.setBindingManager(manager);
			currentType = type;
			manager.exposeBinding(binding);
			return true;
		}

		/**
		 * Exposes a function
		 */
		public boolean visit(ASFunction function) {
			// TODO figure out local function
			if (function.getName() == null) { // local function
				return true;
			}
			MethodBinding binding = new MethodBinding(function, currentType
					.getName().toString()
					+ "." + function.getName().toString());
			binding.setBindingManager(manager);
			manager.exposeBinding(binding);
			return true;
		}

		/**
		 * Exposes a variable declaration
		 */
		public boolean visit(ASVariableDeclaration type) {
			IScopeElement scope = type.getNearestScope();
			if (scope == null) {
				return true;
			}

			String key = type.getName().toString();

			if (scope instanceof ASTypeDeclaration) {
				key = ((ASTypeDeclaration) scope).getName().toString() + "."
						+ key;
			} else if (scope instanceof ASFunction) {
				ASFunction func = (ASFunction) scope;
				ASTypeDeclaration cls = (ASTypeDeclaration) (func)
						.getAncestor(ASTypeDeclaration.class);
				key = cls.getName().toString() + "."
						+ func.getName().toString() + "." + key;
			} else {
				// TODO something?
				return true;
			}
			// else if (scope instanceof ASBlock) {
			//
			// } else if (scope instanceof ASCatchStatement) {
			//
			// } else if (scope instanceof ASForInStatement) {
			//
			// } else if (scope instanceof ASForStatement) {
			//
			// }

			//
			// Create the binding
			//
			VariableBinding binding = new VariableBinding(type, key);
			manager.exposeBinding(binding);

			return true;
		}

		/**
		 * Visits a package.
		 */
		@Override
		public boolean visit(ASPackage type) {

			//
			// Build and expose the binding
			//
			String key = type.getName() == null ? "{default}" : type
					.getName().toString();
			if (manager.getBinding(key) != null) {
				return true;
			}

			PackageBinding binding = new PackageBinding(type, key);
			manager.exposeBinding(binding);

			return true;
		}
	}

	/**
	 * Links up references to their bindings.
	 */
	protected class BindingLinker extends ASDomVisitorBase {

		/** The log */
		private Logger log = Logger.getLogger(BindingLinker.class);

		/** The binding manager */
		private BindingManager manager;

		/**
		 * Creates a new binding linker.
		 * 
		 * @param manager
		 *            the binding manager
		 */
		public BindingLinker(BindingManager manager) {
			this.manager = manager;
		}

		/**
		 * Visits a type reference.
		 */
		public boolean visit(ASType type) {
			if (log.isInfoEnabled()) {
				log.info(type.toString());
			}

			ASTScope scope = ASTScope.getScope(type);
			ASTypeDeclaration declr = scope.getTypeDeclarationForName(type
					.getName());
			if (declr == null) {
				log.warn("unable to resolve " + type.toString());
				return true;
			}

			IBinding binding = manager.getBindingBySource(declr);

			if (binding != null) {
				manager.createBindingLink(type, binding);
			} else {
				log.error("unable to find binding for "
						+ declr.getName().toString());
			}

			return true;
		}

		/**
		 * Visits a field access.
		 */
		public boolean visit(ASFieldAccess faccess) {
			if (log.isInfoEnabled()) {
				log.info(faccess.toString());
			}

			ASExpression expr = faccess.getExpression();

			//
			// Visit child expression, then attempt to get its binding
			//
			IBinding preBind = null;
			if (expr != null) {
				expr.accept(this, true);

				if (expr instanceof ASMethodInvocation) {
					preBind = ((ASMethodInvocation) expr).resolveBinding();
				} else if (expr instanceof ASFieldAccess) {
					preBind = ((ASFieldAccess) expr).resolveBinding();
				}
			}

			//
			//
			//
			IBinding fbinding = null;
			if (preBind != null) {
				switch (preBind.getKind()) {
				case METHOD: {
					IMethodBinding pb = (IMethodBinding) preBind;
					ASFunction f = (ASFunction) pb.getASTNode();

					//
					// Get the type, and the type's binding
					//
					ASType t = f.getReturnType();
					ITypeBinding tb = t.resolveBinding();
					if (tb == null) {
						visit(t);
						tb = t.resolveBinding();
					}

					if (tb == null) {
						log.error("Unable to get binding for " + t.toString());
						return false;
					}

					//
					// Get the binding
					//
					String key = tb.getKey() + "."
							+ faccess.getName().toString();
					fbinding = manager.getBinding(key);
					if (fbinding == null) {
						log.error("Unable to get binding for " + key);
						return false;
					}

					break;
				}

				case TYPE: {
					ITypeBinding pb = (ITypeBinding) preBind;
					ASTypeDeclaration t = (ASTypeDeclaration) pb.getASTNode();
					IBinding tb = manager.getBindingBySource(t);

					if (tb == null) {
						log.error("Unable to get binding for " + t.toString());
						return false;
					}

					String key = tb.getKey() + "."
							+ faccess.getName().toString();
					fbinding = manager.getBinding(key);
					if (fbinding == null) {
						log.error("Unable to get binding for " + key);
						return false;
					}

					break;
				}

				case PACKAGE: {
					IPackageBinding pb = (IPackageBinding) preBind;
					ASPackage pkg = (ASPackage) pb.getASTNode();

					String key = pkg.getName() != null ? pkg
							.getName().toString()
							+ "." + faccess.getName().toString() : faccess
							.getName().toString();
					fbinding = manager.getBinding(key);
					if (fbinding == null) {
						log.error("Unable to get binding for " + key);
						return false;
					}

					break;
				}

				case VARIABLE: {
					IVariableBinding pb = (IVariableBinding) preBind;

					//
					// Get the type, and the type's binding
					//
					ASVariableDeclaration v = (ASVariableDeclaration) pb
							.getASTNode();
					ASType t = v.getType();
					ITypeBinding tb = t.resolveBinding();
					if (tb == null) {
						visit(t);
						tb = t.resolveBinding();
					}

					if (tb == null) {
						log.error("Unable to get binding for " + t.toString());
						return false;
					}

					//
					// Get the binding
					//
					String key = tb.getKey() + "."
							+ faccess.getName().toString();
					fbinding = manager.getBinding(key);
					if (fbinding == null) {
						log.error("Unable to get binding for " + key);
						return false;
					}
					break;
				}
				}
			} else {
				//
				// Either a local variable, a field, a method, a package or
				// a type
				//
				ASTScope scope = ASTScope.getScope(faccess);
				ASTNode node = scope.getNodeForName(faccess
						.getName().toString());

				if (node == null) {
					ASPackage pkg = ASPackage.getPackage(faccess
							.getName().toString());
					if (pkg == null) {
						log.error("Unable to get node for "
								+ faccess.getName().toString());
						return false;
					}

					fbinding = manager.getBindingBySource(pkg);
					if (fbinding == null) {

						return false;
					}

				} else if (node instanceof ASFunction) {
					fbinding = manager.getBindingBySource(node);
				} else if (node instanceof ASVariableDeclaration) {
					fbinding = manager.getBindingBySource(node);
					if (fbinding == null) {
						// FIXME this should never happen
						return false;
					}
				} else if (node instanceof ASTypeDeclaration) {
					fbinding = manager.getBindingBySource(node);
				} else if (node instanceof ASType) {
					ASType t = (ASType) node;
					fbinding = t.resolveBinding();
					if (fbinding == null) {
						visit(t);
						fbinding = t.resolveBinding();
					}

					if (fbinding == null) {
						log.error("Unable to get binding for " + t.toString());
						return false;
					}

				}
			}

			if (fbinding != null) {
				manager.createBindingLink(faccess, fbinding);
				if (faccess.getParent() instanceof ASMethodInvocation) {
					manager.createBindingLink(faccess.getParent(), fbinding);
				}
			}
			return false;
		}

		/**
		 * Visits a method call.
		 */
		public boolean visit(ASMethodInvocation call) {
			if (log.isInfoEnabled()) {
				log.info(call.toString());
			}

			//
			// Visit the expression first
			//
			ASExpression expr = call.getExpression();
			expr.accept(this, true);

			// if (expr instanceof ASFieldAccess) {
			// ASTScope scope = ASTScope.getScope(call);
			// ASName name = ASName.valueOf((ASFieldAccess) expr);
			// ASTypeDeclaration declr = scope.getTypeDeclarationForName(name);
			//
			// if (declr != null) {
			// //
			// // Determine what we're working with
			// //
			// if (call.getParent() instanceof ASNewExpression) {
			//
			// } else { // cast
			// call.setCast(true);
			// }
			//
			// call.setExpression(name);
			// IBinding binding = manager.getBindingBySource(declr);
			// manager.createBindingLink(call, binding);
			// }
			// }

			//
			// Visit the args
			//
			for (ASTNode var : call.getArguments()) {
				var.accept(this, true);
			}

			return false;
		}
	}
}
